package com.tengfei.main.user.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.rabbitmq.client.Channel;
import com.tengfei.common.core.domain.R;
import com.tengfei.common.exception.ServiceException;
import com.tengfei.common.helper.LoginHelper;
import com.tengfei.common.utils.redis.RedisUtils;
import com.tengfei.main.config.RabbitMQConfig;
import com.tengfei.main.exam.domain.Paper;
import com.tengfei.main.exam.domain.PaperQu;
import com.tengfei.main.exam.domain.PaperUser;
import com.tengfei.main.exam.domain.PaperUserQu;
import com.tengfei.main.exam.domain.vo.PaperVo;
import com.tengfei.main.exam.mapper.PaperMapper;
import com.tengfei.main.exam.mapper.PaperQuMapper;
import com.tengfei.main.exam.mapper.PaperUserMapper;
import com.tengfei.main.exam.mapper.PaperUserQuMapper;
import com.tengfei.main.question.domain.vo.ObjectiveVo;
import com.tengfei.main.question.domain.vo.SubjectiveVo;
import com.tengfei.main.question.mapper.ObjectiveMapper;
import com.tengfei.main.question.mapper.SubjectiveMapper;
import com.tengfei.main.system.domain.SysUser;
import com.tengfei.main.system.mapper.SysUserMapper;
import com.tengfei.main.user.domain.vo.exam.*;
import com.tengfei.main.user.service.IUserExamService;
import com.tengfei.main.user.service.UserAsyncService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.*;

/**
 * @author feige
 * @version v1.0
 * @date 2022-09-13-21:28
 * @description
 */
@Slf4j
@Service
public class UserExamServiceImpl implements IUserExamService {

    private static final String USER_EXAM = "user_exam:";

    private static final String QU_LIST = "qu_list:";

    private static final String QU_INFO_LIST = "qu_info_list:";

    @Resource
    private PaperUserMapper paperUserMapper;
    @Resource
    private PaperMapper paperMapper;
    @Resource
    private PaperQuMapper paperQuMapper;
    @Resource
    private UserAsyncService userAsyncService;
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private PaperUserQuMapper userQuMapper;
    @Resource
    private ObjectiveMapper objectiveMapper;
    @Resource
    private SubjectiveMapper subjectiveMapper;
    @Resource
    private SysUserMapper userMapper;

    /**
     * 查询用户试卷列表
     *
     * @param paperStatus
     * @param status
     * @param paperName
     * @return
     */
    @Override
    public List<UserPaperVo> getPaperList(Integer paperStatus, Integer status, String paperName) {
        Long userId = LoginHelper.getUserId();
        List<UserPaperVo> userPaperVos = paperUserMapper.selectUserPapers(paperStatus, status, paperName, userId);
        return userPaperVos;
    }

    /*
     * 查询所有试卷列表
     */
    @Override
    public List<PaperVo> getAllPaperList(Integer paperStatus, String paperName) {
        List<PaperVo> paperVos = paperMapper.selectVoList(new LambdaQueryWrapper<Paper>()
                .eq(ObjectUtil.isNotNull(paperStatus), Paper::getPaperStatus, paperStatus)
                .like(ObjectUtil.isNotNull(paperName), Paper::getPaperName, paperName));
        return paperVos;
    }

    @Override
    public R<UserPaperVo> getUserPaperById(Long paperId) {
        Long userId = LoginHelper.getUserId();
        // 查询试卷信息
        PaperUser paperUser = paperUserMapper.selectOne(new LambdaQueryWrapper<PaperUser>()
                .eq(PaperUser::getUserId, userId)
                .eq(PaperUser::getPaperId, paperId));
        if (ObjectUtil.isNull(paperUser)) {
            return R.fail("你暂无本场考试信息");
        }
        Paper paper = paperMapper.selectById(paperId);
        if (ObjectUtil.isNull(paper) || ObjectUtil.isNull(paperUser)) {
            R.fail("你暂无本场考试信息");
        }
        UserPaperVo userPaperVo = new UserPaperVo();
        userPaperVo.setPaperId(paperId);
        userPaperVo.setPaperName(paper.getPaperName());
        userPaperVo.setPaperDuration(paper.getPaperDuration());
        userPaperVo.setPaperNowScore(paper.getPaperNowScore());
        userPaperVo.setStatus(paperUser.getUserPaperStatus());
        return R.ok(userPaperVo);
    }

    /**
     * 初始化试卷
     *
     * @return
     */
    @Override
    public R<UserPaperInitVo> initPaper(Long paperId) {
        Long userId = LoginHelper.getUserId();
        // 查询试卷信息
        PaperUser paperUser = paperUserMapper.selectOne(new LambdaQueryWrapper<PaperUser>()
                .eq(PaperUser::getUserId, userId)
                .eq(PaperUser::getPaperId, paperId));
        if (ObjectUtil.isNull(paperUser)) {
            return R.fail("你暂无本场考试信息");
        }
        UserPaperInitVo initVo = new UserPaperInitVo();
        // 查询试卷信息
        Paper paper = paperMapper.selectById(paperId);
        if (paper.getPaperStartTime().getTime() > new Date().getTime()) {
            return R.fail("考试还未开始");
        }
        if (paper.getPaperEndTime().getTime() < new Date().getTime()) {
            return R.fail("考试已结束");
        }
        if (paperUser.getUserPaperStatus() == 1) {
            // 中途退出，继续考试
            // 考试剩余时间
            long ttl = RedisUtils.getTimeToLive(USER_EXAM + QU_LIST + paperId + ":" + userId + ":ttl");
            if (ttl <= 0) {
                R.fail("你的考试已结束");
            }
            // 从redis中获取数据（获取通配）
            List<Long> quIds = RedisUtils.getCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":quIds");
            if (ObjectUtil.isNull(quIds) || quIds.size() <= 0) {
                return R.fail("考试已结束");
            }
            List<UserQuVo> list = new ArrayList<>();
            for (Long quId : quIds) {
                UserQuVo userQu = RedisUtils.getCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":" + quId);
                list.add(userQu);
            }
            initVo.setTtl(ttl / 1000 + 2);
            initVo.setUserQus(list);
            return R.ok(initVo);
        } else if (paperUser.getUserPaperStatus() == 0) {
            // 初始化试卷，开始考试
            // 查询试卷题目列表
            List<PaperQu> paperQus = paperQuMapper.selectList(new LambdaQueryWrapper<PaperQu>()
                    .eq(PaperQu::getPaperId, paperUser.getPaperId())
                    .orderByAsc(PaperQu::getQuType));
            // TODO 题目乱序
            List<UserQuVo> userPaperQuVos = BeanUtil.copyToList(paperQus, UserQuVo.class);
            List<Long> quIds = new ArrayList<>();
            userPaperQuVos.forEach(item -> {
                // 将数据存入redis（向后偏移10分钟，冗余防失误）
                RedisUtils.setCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":" + item.getQuId(),
                        item, Duration.ofMinutes(paper.getPaperDuration() + 10));
                quIds.add(item.getQuId());
            });
            // 将UserQu的id存入redis
            RedisUtils.setCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":quIds", quIds);
            //（向后偏移10分钟，冗余防失误）
            RedisUtils.expire(USER_EXAM + QU_LIST + paperId + ":" + userId + ":quIds",
                    Duration.ofMinutes(paper.getPaperDuration() + 10));

            // 更改用户试卷状态为考试中...
            paperUser.setUserPaperStatus(1);
            paperUser.setUserStartTime(new Date());
            paperUserMapper.updateById(paperUser);

            // 是否包含该key（使用redis-key的通配）
            if (!RedisUtils.hasKeys(USER_EXAM + QU_INFO_LIST + paperId + ":*")) {
                long duration = paper.getPaperEndTime().getTime() - paper.getPaperStartTime().getTime();
                // 转成秒
                duration /= 1000;
                // 异步获取题目详细信息列表，并存入redis（乱序后的列表）（在此基础上考试时长）
                userAsyncService.initExamQuInfoList(USER_EXAM + QU_INFO_LIST + paperId + ":", paperQus,
                        duration + paper.getPaperDuration() * 60);
            }

            // 将考试信息发布到rabbitmq延迟队列（延迟20s防止与手动提交冲突）
            delayProduct(paperUser.getId(), paper.getPaperDuration() * 60 + 20);
            initVo.setUserQus(userPaperQuVos);
            // 设定考试剩余时间
            RedisUtils.setCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":ttl", "考试剩余时间",
                    Duration.ofMinutes(paper.getPaperDuration()));
            initVo.setTtl((long) (paper.getPaperDuration() * 60));
            return R.ok(initVo);
        } else {
            return R.fail("该考试您已交卷...");
        }
    }

    /**
     * 查询试卷题目详细信息
     *
     * @param quId
     * @param paperId
     * @return
     */
    @Override
    public R<Object> quInfo(Long quId, Long paperId, Integer quType) {
        Object quInfo = RedisUtils.getCacheObject(USER_EXAM + QU_INFO_LIST + paperId + ":" + quId);
        if (ObjectUtil.isNull(quInfo)) {
            return R.fail("试卷/题目不存在/考试已结束");
        }
        Map<String, Object> map = new HashMap<>();
        if (isObj(quType)) {
            UserPaperObjVo objVo = (UserPaperObjVo) quInfo;
            objVo.setObjAnswer("");
            objVo.setObjAnswerContent("");
            objVo.setObjAnswerEnclosure("");
            map.put("quInfo", objVo);
        } else {
            UserPaperSubVo subVo = (UserPaperSubVo) quInfo;
            subVo.setSubAnswer("");
            subVo.setSubAnswerContent("");
            subVo.setSubAnswerEnclosure("");
            map.put("quInfo", subVo);
        }
        // 判断用户该题是否已答，并将用户答案返回
        Long userId = LoginHelper.getUserId();
        UserQuVo userQu = RedisUtils.getCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":" + quId);
        map.put("userAnswer", userQu.getUserAnswer());
        return R.ok(map);
    }

    /**
     * 上传用户答案
     *
     * @param quId
     * @param paperId
     * @param userAns
     * @return
     */
    @Override
    public R<Void> uploadQuAns(Long quId, Long paperId, String userAns) {
        Long userId = LoginHelper.getUserId();
        UserQuVo userQu = RedisUtils.getCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":" + quId);
        if (ObjectUtil.isNull(userQu)) {
            return R.fail("考试已结束/题目不存在");
        }
        userQu.setUserAnswer(userAns);
        RedisUtils.setCacheObject(USER_EXAM + QU_LIST + paperId + ":" + userId + ":" + quId, userQu, true);
        return R.ok("保存成功");
    }

    /**
     * 交卷
     *
     * @param paperId
     * @return
     */
    @Override
    public R<Void> submitPaper(Long paperId) {
        Long userId = LoginHelper.getUserId();
        PaperUser paperUser = paperUserMapper.selectOne(new LambdaQueryWrapper<PaperUser>()
                .eq(PaperUser::getPaperId, paperId)
                .eq(PaperUser::getUserId, userId));
        if (ObjectUtil.isNull(paperUser)) {
            return R.fail("考试记录不存在");
        }
        paperUser.setUserPaperStatus(2);
        paperUser.setUserEndTime(new Date());
        boolean res = paperUserMapper.updateById(paperUser) > 0;
        calculateScore(paperUser);
        return res ? R.ok("交卷成功") : R.fail("交卷失败");
    }

    /**
     * 试卷作答详情
     *
     * @param paperId
     * @return
     */
    @Override
    public R<List> ansInfoList(Long paperId) {
        Paper paper = paperMapper.selectById(paperId);
        if (ObjectUtil.isNull(paper)) {
            return R.fail("试卷不存在");
        }
        if (paper.getPaperEndTime().getTime() > new Date().getTime()) {
            return R.fail("未达到总考试结束时间，请稍后查看");
        }
        Long userId = LoginHelper.getUserId();
        List<PaperUserQu> paperUserQus = userQuMapper.selectList(new LambdaQueryWrapper<PaperUserQu>()
                .eq(PaperUserQu::getPaperId, paper.getPaperId())
                .eq(PaperUserQu::getUserId, userId));

        List ansList = new ArrayList(paperUserQus.size());
        paperUserQus.forEach(item -> {
            if (isObj(item.getQuType())) {
                UserPaperObjVo objInfo = RedisUtils.getCacheObject(USER_EXAM + QU_INFO_LIST + paperId +
                        ":" + item.getQuId());
                UserAnsObjVo userAnsObjVo = null;
                if (ObjectUtil.isNotNull(objInfo)) {
                    userAnsObjVo = BeanUtil.copyProperties(objInfo, UserAnsObjVo.class);
                } else {
                    ObjectiveVo objectiveVo = objectiveMapper.selectVoById(item.getQuId());
                    userAnsObjVo = BeanUtil.copyProperties(objectiveVo, UserAnsObjVo.class);
                    PaperQu paperQu = paperQuMapper.selectOne(new LambdaQueryWrapper<PaperQu>()
                            .eq(PaperQu::getPaperId, paperId)
                            .eq(PaperQu::getQuId, item.getQuId()));
                    userAnsObjVo.setQuScore(ObjectUtil.isNotNull(paperQu) ? paperQu.getQuScore() : 0);
                }
                userAnsObjVo.setUserAnswer(item.getUserAnswer());
                userAnsObjVo.setUserScore(item.getUserScore());
                ansList.add(userAnsObjVo);
            } else {
                UserPaperSubVo subInfo = RedisUtils.getCacheObject(USER_EXAM + QU_INFO_LIST + paperId +
                        ":" + item.getQuId());
                UserAnsSubVo userAnsSubVo = null;
                if (ObjectUtil.isNotNull(subInfo)) {
                    userAnsSubVo = BeanUtil.copyProperties(subInfo, UserAnsSubVo.class);
                } else {
                    SubjectiveVo subjectiveVo = subjectiveMapper.selectVoById(item.getQuId());
                    userAnsSubVo = BeanUtil.copyProperties(subjectiveVo, UserAnsSubVo.class);
                    PaperQu paperQu = paperQuMapper.selectOne(new LambdaQueryWrapper<PaperQu>()
                            .eq(PaperQu::getPaperId, paperId)
                            .eq(PaperQu::getQuId, item.getQuId()));
                    userAnsSubVo.setQuScore(ObjectUtil.isNotNull(paperQu) ? paperQu.getQuScore() : 0);
                }
                userAnsSubVo.setUserAnswer(item.getUserAnswer());
                userAnsSubVo.setUserScore(item.getUserScore());
                ansList.add(userAnsSubVo);
            }
        });
        return R.ok(ansList);
    }

    /*
     * 试卷作答详情
     */
    @Override
    public R<Void> examInfo(Long paperId) {
        Paper paper = paperMapper.selectById(paperId);
        if (ObjectUtil.isNull(paper)) {
            return R.fail("试卷不存在");
        }
        if (paper.getPaperEndTime().getTime() > new Date().getTime()) {
            return R.fail("未达到总考试结束时间，请稍后查看");
        }
        return R.ok("允许查看考试结果");
    }

    /*
     * 用户排名信息
     */
    @Override
    public R<List<Map<String, Object>>> userRank(Long paperId) {
        Long userId = LoginHelper.getUserId();
        // 查询试卷信息
        PaperUser paperUser = paperUserMapper.selectOne(new LambdaQueryWrapper<PaperUser>()
                .eq(PaperUser::getUserId, userId)
                .eq(PaperUser::getPaperId, paperId));
        if (ObjectUtil.isNull(paperUser)) {
            return R.fail("你暂无本场考试信息");
        }
        Paper paper = paperMapper.selectById(paperId);
        if (ObjectUtil.isNull(paper)) {
            return R.fail("试卷不存在");
        }
        if (paper.getPaperEndTime().getTime() > new Date().getTime()) {
            return R.fail("未达到总考试结束时间，请稍后查看");
        }
        List<PaperUser> paperUsers = paperUserMapper.selectList(new LambdaQueryWrapper<PaperUser>()
                .eq(PaperUser::getPaperId, paperId)
                .orderByDesc(PaperUser::getUserPaperScore));
        List<Map<String, Object>> list = new ArrayList<>(paperUsers.size());
        int index = 0;
        for (PaperUser item : paperUsers) {
            SysUser sysUser = userMapper.selectById(item.getUserId());
            if (ObjectUtil.isNotNull(sysUser)) {
                index++;
                Map<String, Object> map = new HashMap<>();
                String name = sysUser.getName();
                map.put("name", name);
                map.put("score", item.getUserPaperScore());
                map.put("rank", index);
                list.add(map);
            }
        }
        return R.ok(list);
    }


    /**
     * 延迟队列生产者，将用户考试信息放入延迟队列
     *
     * @param id        代表唯一特性的id，此处使用用户试卷绑定的id
     * @param delayTime 延迟队列延迟时间/s（考试时间）
     */
    private void delayProduct(Long id, Integer delayTime) {
        /* 确认的回调 确认消息是否到达 Broker 服务器 其实就是是否到达交换器
         * 如果发送时候指定的交换器不存在 ack 就是 false 代表消息不可达
         */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            log.info("correlationData：{} , ack:{}", correlationData.getId(), ack);
            if (!ack) {
                log.error("消息未匹配到交换机，消息不可达，进行响应补救措施，例如：重试");
            }
        });
        /* 消息失败的回调
         * 例如消息已经到达交换器上，但路由键匹配任何绑定到该交换器的队列，会触发这个回调，此时 replyText: NO_ROUTE
         */
        rabbitTemplate.setReturnsCallback(returnedMessage -> {
            log.info("message:{}; replyCode: {}; replyText: {} ; exchange:{} ; routingKey:{}",
                    returnedMessage.getMessage(), returnedMessage.getReplyCode(), returnedMessage.getReplyText(),
                    returnedMessage.getExchange(), returnedMessage.getRoutingKey());
            log.error("消息未匹配到路由键，消息不可达");
        });
        // 在实际中ID 应该是全局唯一 能够唯一标识消息 消息不可达的时候触发ConfirmCallback回调方法时可以获取该值，进行对应的错误处理
        CorrelationData correlationData = new CorrelationData(id.toString());
        rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE, RabbitMQConfig.DELAY_ROUTING_KEY
                , id, message -> {
                    // 设置延迟时间
                    log.info("延迟时间{}", delayTime);
                    message.getMessageProperties().setDelay(delayTime * 1000);
                    return message;
                }, correlationData);
    }


    /**
     * 延迟队列TTL到期回调
     *
     * @param id      代表唯一特性的id，此处使用用户试卷绑定的id
     * @param message
     * @param channel
     */
    @RabbitListener(queues = {RabbitMQConfig.DELAY_QUEUE})
    public void consumer(Long id, Message message, Channel channel) {
        log.info("{}：到达交卷时间", id);
        // 查询数据库
        PaperUser paperUser = paperUserMapper.selectById(id);
        if (ObjectUtil.isNotNull(paperUser) && paperUser.getUserPaperStatus() == 1) {
            // 更改答题状态
            paperUser.setUserPaperStatus(2);
            paperUser.setUserEndTime(new Date());
            paperUserMapper.updateById(paperUser);
            // 计算分数
            calculateScore(paperUser);
        }
    }


    // 计算分数
    private void calculateScore(PaperUser paperUser) {
        // 题目简略信息列表
        String listPrefixKey = new StringBuffer(USER_EXAM)
                .append(QU_LIST)
                .append(paperUser.getPaperId())
                .append(":")
                .append(paperUser.getUserId())
                .append(":")
                .toString();
        String infoPrefixInfo = new StringBuffer(USER_EXAM)
                .append(QU_INFO_LIST)
                .append(paperUser.getPaperId())
                .append(":")
                .toString();
        // 获取缓存中题目列表
        List<Long> quIds = RedisUtils.getCacheObject(listPrefixKey + "quIds");
        if (ObjectUtil.isNull(quIds) || quIds.size() <= 0) {
            throw new ServiceException("考试已结束");
        }
        // 保存数据并打分
        userAsyncService.saveQuAndCalculate(quIds, paperUser, listPrefixKey, infoPrefixInfo);
    }


    // 非空判断+界限判断+是否为客观题
    private boolean isObj(Integer quType) {
        if (quType.intValue() < 1 || quType.intValue() > 5) {
            throw new ServiceException("题目类型不符合规范");
        } else if (quType.intValue() >= 1 && quType.intValue() <= 3) {
            return true;
        } else {
            return false;
        }
    }
}
